package org.xtext.runtime.library;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.xtext.runtime.formula.Predicate;
import org.xtext.runtime.interpreter.ActionParam;
import org.xtext.runtime.interpreter.Agent;
import org.xtext.runtime.interpreter.FailedActionException;
import org.xtext.runtime.interpreter.NoSuchActionException;
import org.xtext.runtime.library.Library.ACTION;
import org.xtext.runtime.library.Library.FORMULA;
import org.xtext.runtime.library.Library.SENSOR;
import org.xtext.runtime.library.Library.TERM;
import org.xtext.runtime.term.ITerm;
import org.xtext.runtime.term.Variable;
import org.xtext.runtime.type.IType;
import org.xtext.runtime.type.Type;
import org.xtext.runtime.type.TypeException;


public class LibraryWrapper {
	private Library library;
	private Map<String, LinkedList<Method>> actions = new HashMap<String,LinkedList<Method>>();;
	private Map<String, LinkedList<Method>> formulae = new HashMap<String,LinkedList<Method>>();;
	private Map<String, LinkedList<Method>> terms = new HashMap<String,LinkedList<Method>>();;
	private List<Method> sensors = new LinkedList<Method>();
	
	/**
	 * Main factory method for LibraryWrapper class. This method takes a class name, resolves
	 * it to a class, instantiates the class, and then inspects the class for methods that are
	 * annotated with SENSOR and ACTION annotations.
	 * 
	 * @param url the classname
	 * 
	 * @return an initialised instance of a LibraryWrapper for the class specified.
	 * @throws LibraryException 
	 */
	public static LibraryWrapper newInstance(String url) throws LibraryException {
		Class<?> clazz = null;
		try {
			clazz = Class.forName(url);
		} catch (ClassNotFoundException e) {
			throw new LibraryException(e);
		}
		
		Library lib = null;
		try {
			lib = (Library) clazz.newInstance();
		} catch (InstantiationException e) {
			throw new LibraryException(e);
		} catch (IllegalAccessException e) {
			throw new LibraryException(e);
		}
		return new LibraryWrapper(lib); 
	}
	
	/**
	 * Constructor - analyses the library instance and identifies any actions or
	 * sensors that have been declared. It stores actions in a map to allow fast
	 * invocation of actions and stores sensorsr in a list as they are typically
	 * executed serially.
	 * 
	 * @param library the library to be wrapped 
	 * @throws LibraryException 
	 */
	private LibraryWrapper(Library library) throws LibraryException {
		this.library = library;
		
		// Analyse library class to extract key sensor and action methods
		for (Method method : library.getClass().getMethods()) {
			for (Annotation annotation : method.getDeclaredAnnotations()) {
				if (annotation instanceof ACTION) {
					if (method.getReturnType().getName().equals("boolean")) {
						LinkedList<Method> list = actions.get(method.getName());
						if (list == null) {
							list = new LinkedList<Method>();
							actions.put(method.getName(), list);
						}
						list.addLast(method);
					} else {
						throw new LibraryException("Invalid Action method signature: return value must be a boolean.");
					}
				} else if (annotation instanceof SENSOR) {
					if (method.getReturnType().getName().equals("java.util.List") &&
							method.getParameterTypes().length == 0) {
						sensors.add(method);
					} else {
						throw new LibraryException("Invalid Sensor method signature: return value must be List<Predicate> and there must be no parameters.");
					}
				} else if (annotation instanceof TERM) {
					LinkedList<Method> list = terms.get(method.getName());
					if (list == null) {
						list = new LinkedList<Method>();
						terms.put(method.getName(), list);
					}
					list.addLast(method);
				} else if (annotation instanceof FORMULA) {
					if (method.getReturnType().getName().equals("boolean")) {
						LinkedList<Method> list = formulae.get(method.getName());
						if (list == null) {
							list = new LinkedList<Method>();
							formulae.put(method.getName(), list);
						}
						list.addLast(method);
					} else {
						throw new LibraryException("Invalid formula signature: return value must be a boolean.");
					}
				}
			}
		}
	}
	
	public Method getMethodForAction(Predicate operation) throws NoSuchActionException {

		LinkedList<Method> methods = actions.get(operation.identifier());
		if (methods != null) {
			for (Method method: methods) {
				if (matchArguments(method, operation.terms())) {
					return method;
				}
			}
		}

		throw new NoSuchActionException("Could not find action in: " + 
				library.getClass().getName() + " to match action: " + operation); 
	}


	public boolean invokePredicate(Predicate predicate) throws LibraryException {
		LinkedList<Method> methods = formulae.get(predicate.identifier());
		if (methods != null) {
			for (Method method: methods) {
				Object[] args = getArguments(method, predicate.terms());
				if (args != null) {
					// We have located an appropriate method, so invoke it
					try {
						return (Boolean) method.invoke(library, args);
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					} catch (IllegalArgumentException e) {
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						e.printStackTrace();
					}

					throw new LibraryException("Could not evaluate formula: " + predicate + " in: " +  library.getClass().getName()); 
				}
			}
		}

		throw new LibraryException("Could not find method in: " + 
				library.getClass().getName() + " to match predicate: " + predicate); 
	}

	public Object invokeTerm(Predicate function) throws LibraryException, NoSuchActionException {
		LinkedList<Method> methods = terms.get(function.identifier());
		if (methods != null) {
			for (Method method: methods) {
				Object[] args = getArguments(method, function.terms());
				if (args != null) {
					// We have located an appropriate method, so invoke it
					try {
						return method.invoke(library, args);
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					} catch (IllegalArgumentException e) {
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						e.printStackTrace();
					}

					throw new LibraryException("Could not evaluate term: " + function + " in: " +  library.getClass().getName()); 
				}
			}
		}

		throw new NoSuchActionException("Could not find method in: " + 
				library.getClass().getName() + " to match term: " + function); 
	}
	
	/**
	 * Resolves a predicate to a method (if one exists) and invokes the method on the wrapped library
	 * object.
	 * 
	 * @param operation a predicate representing the action to be invoked.
	 * @param newBindings 
	 * @return 
	 * @throws FailedActionException 
	 * @throws NoSuchActionException 
	 */
	@SuppressWarnings({ "rawtypes" })
	public boolean invokeAction(Predicate operation) throws FailedActionException, NoSuchActionException {
		LinkedList<Method> methods = actions.get(operation.identifier());
		if (methods != null) {
			for (Method method: methods) {
				Object[] args = getArguments(method, operation.terms());
				if (args != null) {
					// We have located an appropriate method, so invoke it
					try {
						boolean result = (Boolean) method.invoke(library, args);
						
						// Now, extract any return values and put them into the newBindings 
						// bindings (but ony if the action was successful).
						if (result) {
							for (int i=0; i < args.length; i++) {
								ITerm it = operation.termAt(i);
								if (args[i] != null && args[i].getClass().getName().equals(ActionParam.class.getCanonicalName())) {
									((Variable) it).assign(((ActionParam) args[i]).toLogic());
								}
							}
						}
						
						return result;
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					} catch (IllegalArgumentException e) {
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						e.printStackTrace();
					} catch (TypeException e) {
						e.printStackTrace();
					}

					throw new FailedActionException("Could not execute action: " + operation + " in: " +  library.getClass().getName()); 
				}
			}
		} else {
			System.out.println(">>>>> NO METHODS FOR ACTION: " + operation.identifier());
		}

		throw new NoSuchActionException("Could not find action in: " + library.getClass().getName() + " to match action: " + operation); 
	}

	public Set<Variable> getReturnVariables(Predicate operation) throws NoSuchActionException {
		Set<Variable> variables = new HashSet<Variable>();

		Method method = this.getMethodForAction(operation);
		Class<?>[] parameters = method.getParameterTypes();
		for (int i=0; i<parameters.length; i++) {
			if (parameters[i].getName().equals("astra.interpreter.ActionParam")) {
				variables.add((Variable) operation.termAt(i));
			}
			
		}
		return variables;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private static Object[] getArguments(Method method, ITerm[] iTerms) {
		Class<?>[] parameters = method.getParameterTypes();
		if (parameters.length == iTerms.length) {
			int i=0;
			boolean success = true;
			Object[] args = new Object[parameters.length];
			while (i<parameters.length && success) {
				ITerm term = iTerms[i];
				if (parameters[i].getName().equals(ActionParam.class.getCanonicalName())) {
					if (Variable.class.isInstance(term)) {
						try {
							args[i] = parameters[i].newInstance();
						} catch (InstantiationException e) {
							e.printStackTrace();
							success = false;
						} catch (IllegalAccessException e) {
							e.printStackTrace();
							success = false;
						}
						
						Variable v = (Variable) term;
						if (!v.isFree()) {
							((ActionParam) args[i]).set(v.value().value());
						}
					} else {
						System.err.println("[LibraryWrapper.invokeAction] ActionParam should be mapped to type: " + parameters[i].getCanonicalName() + " for action:" + method.getName());
						success = false;
					}
				} else {				
					if (Variable.class.isInstance(term)) {
						term = ((Variable) term).value();
					}

					if (parameters[i].getName().equals("java.lang.String")) {
						if (org.xtext.runtime.term.List.class.isInstance(term)) {
							success = false;
						} else {
							args[i] = term.value().toString();
						}
					} else if (parameters[i].getName().equals("int")) {
						if (Type.isNumericType(term)) {
							args[i] = ((Number) term.value()).intValue();
						} else {
							success = false;
						}
					} else if (parameters[i].getName().equals("long")) {
						if (Type.isNumericType(term)) {
							args[i] = ((Number) term.value()).longValue();
						} else {
							success = false;
						}
					} else if (parameters[i].getName().equals("float")) {
						if (Type.isNumericType(term)) {
							args[i] = ((Number) term.value()).floatValue();
						} else {
							success = false;
						}
					} else if (parameters[i].getName().equals("double")) {
						if (Type.isNumericType(term)) {
							args[i] = ((Number) term.value()).doubleValue();
						} else {
							success = false;
						}
					} else if (parameters[i].isAssignableFrom(List.class)) {
						if (term instanceof org.xtext.runtime.term.List) {
							args[i] = ((org.xtext.runtime.term.List) term).asList();
						} else {
							success = false;
						}
					} else {
						System.err.println("[LibraryWrapper.invokeAction] Unhandled type: " + parameters[i].getCanonicalName() + " for action:" + method.getName());
						success = false;
					}
				}
				
				i++;
			}
			
			return success ? args:null;
		}
		return null;
	}

	private static boolean matchArguments(Method method, ITerm[] iTerms) {
		Class<?>[] parameters = method.getParameterTypes();
		if (parameters.length == iTerms.length) {
			for (int i=0; i<parameters.length; i++) {
				// deal with type mappings here...
				if (parameters[i].getName().equals(ActionParam.class.getCanonicalName())) {
					System.out.println("[LibraryWrapper.matchArguments] action param: " + parameters[i]);
					try {
						System.out.println("method: " + parameters[i].getMethod("get"));
					} catch (NoSuchMethodException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				} else if (parameters[i].getName().equals(List.class.getCanonicalName())) {
					if (!iTerms[i].type().equals(Type.listType)) {
						System.err.println("[LibraryWrapper.matchArguments] expected list but got: " + iTerms[i].type());
						return false; 
					}
//					throw new UnsupportedOperationException();
				} else if (parameters[i].getName().equals(String.class.getCanonicalName())) {
					return true;
				} else if (!parameters[i].getName().equals(iTerms[i].type().type())) {
					throw new RuntimeException("[LibraryWrapper.matchArguments] mismatch: '" + parameters[i].getName() + "' / '" + iTerms[i].type() + "'");
				}
			}
		}
		return true;
	}

	/**
	 * Sense the environment of the agent - this involves invoking all of the methods that
	 * have been declared as sensors in the wrapped library.
	 * 
	 * @return a list of all the predicates representing beliefs generated about the environment from this library
	 */
	@SuppressWarnings("unchecked")
	public List<Predicate> senseEnvironment() {
		List<Predicate> list = new LinkedList<Predicate>();
		for (Method method : sensors) {
			try {
				list.addAll((List<Predicate>) method.invoke(library, new Object[0]));
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
				e.getCause().printStackTrace();
			}
		}
		return list;
	}
	
	public void setAgent(Agent agent) {
		library.setAgent(agent);
	}

	public IType getTermType(Predicate function) {
		LinkedList<Method> methods = terms.get(function.identifier());
		if (methods != null) {
			for (Method method: methods) {
				if (matchArguments(method, function.terms())) {
					// We have located an appropriate method, so invoke it
					try {
						return Type.getType(method.getReturnType());
					} catch (IllegalArgumentException e) {
						e.printStackTrace();
					}

				}
			}
		}
		
		System.err.println("[LibraryWrapper.getTermType] Could not match to a method: " + function);

		return null; 
	}

	public LibraryWrapper copy() throws LibraryException, InstantiationException, IllegalAccessException {
		return new LibraryWrapper(library.getClass().newInstance());
	}

	public String className() {
		return this.library.getClass().getSimpleName();
	}
}
